Skip to content

feat: add new endpoints for fetching feature flags and feature subgraphs by federated graph#2729

Open
JivusAyrus wants to merge 14 commits intomainfrom
suvij/eng-9327-controlplane-split-feature-flag-data-out-of
Open

feat: add new endpoints for fetching feature flags and feature subgraphs by federated graph#2729
JivusAyrus wants to merge 14 commits intomainfrom
suvij/eng-9327-controlplane-split-feature-flag-data-out-of

Conversation

@JivusAyrus
Copy link
Copy Markdown
Member

@JivusAyrus JivusAyrus commented Apr 1, 2026

Summary by CodeRabbit

  • New Features
    • Two new API endpoints for federated graph feature management: GetFeatureFlagsInLatestCompositionByFederatedGraph retrieves feature flags in the latest composition; GetFeatureSubgraphsByFederatedGraph retrieves feature subgraphs with pagination and search support.

Checklist

  • I have discussed my proposed changes in an issue and have received approval to proceed.
  • I have followed the coding standards of the project.
  • Tests or benchmarks have been added or updated.
  • Documentation has been updated on https://github.com/wundergraph/docs-website.
  • I have read the Contributors Guide.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This PR refactors feature flags and feature subgraphs out of GetFederatedGraphByNameResponse into dedicated RPC endpoints. It adds GetFeatureFlagsInLatestCompositionByFederatedGraph and GetFeatureSubgraphsByFederatedGraph service methods, implements corresponding handlers and repository queries, extracts workspace assembly logic into WorkspaceService, and updates studio UI to fetch this data separately.

Changes

Cohort / File(s) Summary
Protocol Buffers & Generated Bindings
proto/wg/cosmo/platform/v1/platform.proto, connect/src/wg/cosmo/platform/v1/platform_pb.ts, connect/src/wg/cosmo/platform/v1/platform_connect.ts, connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts
Removed featureFlagsInLatestValidComposition and featureSubgraphs from GetFederatedGraphByNameResponse; added four new message types and two new RPC methods to PlatformService for dedicated feature-flag and feature-subgraph retrieval with pagination/search support.
Platform Service Implementation
controlplane/src/core/bufservices/PlatformService.ts, controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts, controlplane/src/core/bufservices/feature-flag/getFeatureSubgraphsByFederatedGraph.ts, controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.ts
Added two new RPC handlers with RBAC enforcement and error handling; refactored getFederatedGraphByName to remove feature-flag/subgraph data assembly and delegation to dedicated methods.
Repository Refactoring
controlplane/src/core/repositories/FeatureFlagRepository.ts, controlplane/src/core/repositories/FederatedGraphRepository.ts, controlplane/src/core/repositories/SubgraphRepository.ts
Enhanced FeatureFlagRepository with new getFeatureSubgraphsByFederatedGraph method, optional subgraph exclusion, and expanded RBAC short-circuiting; converted RBAC methods to static on FederatedGraphRepository and SubgraphRepository; refactored SubgraphRepository.listByFederatedGraph to use unified join-based query.
Workspace Service
controlplane/src/core/bufservices/workspace/getWorkspace.ts, controlplane/src/core/services/WorkspaceService.ts
Extracted workspace assembly logic into new WorkspaceService class with namespace/federated-graph/subgraph hierarchy building; updated getWorkspace handler to delegate to the service.
Studio UI
studio/src/components/layout/graph-layout.tsx, studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx, studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/schema/index.tsx, studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/schema/sdl.tsx, studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/subgraphs.tsx
Removed feature-flag/subgraph context properties; added dedicated useQuery calls to getFeatureFlagsInLatestCompositionByFederatedGraph and getFeatureSubgraphsByFederatedGraph; replaced client-side filtering with server-driven pagination and search.
Tests
controlplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.ts, controlplane/test/feature-flag/get-feature-subgraphs-by-federated-graph.test.ts
Added comprehensive test suites covering feature-flag and feature-subgraph retrieval across multiple scenarios including pagination, search filtering, RBAC, and edge cases.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately and concisely describes the main purpose of the changeset: adding new endpoints for fetching feature flags and feature subgraphs by federated graph.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 1, 2026

Codecov Report

❌ Patch coverage is 58.51240% with 251 lines in your changes missing coverage. Please review.
✅ Project coverage is 40.44%. Comparing base (0b0d42d) to head (43461ef).

Files with missing lines Patch % Lines
controlplane/src/core/services/WorkspaceService.ts 5.98% 110 Missing ⚠️
[...zationSlug]/[namespace]/graph/[slug]/subgraphs.tsx](https://app.codecov.io/gh/wundergraph/cosmo/pull/2729?src=pr&el=tree&filepath=studio%2Fsrc%2Fpages%2F%5BorganizationSlug%5D%2F%5Bnamespace%5D%2Fgraph%2F%5Bslug%5D%2Fsubgraphs.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wundergraph#diff-c3R1ZGlvL3NyYy9wYWdlcy9bb3JnYW5pemF0aW9uU2x1Z10vW25hbWVzcGFjZV0vZ3JhcGgvW3NsdWddL3N1YmdyYXBocy50c3g=) 0.00% 43 Missing ⚠️
[...ationSlug]/[namespace]/graph/[slug]/playground.tsx](https://app.codecov.io/gh/wundergraph/cosmo/pull/2729?src=pr&el=tree&filepath=studio%2Fsrc%2Fpages%2F%5BorganizationSlug%5D%2F%5Bnamespace%5D%2Fgraph%2F%5Bslug%5D%2Fplayground.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wundergraph#diff-c3R1ZGlvL3NyYy9wYWdlcy9bb3JnYW5pemF0aW9uU2x1Z10vW25hbWVzcGFjZV0vZ3JhcGgvW3NsdWddL3BsYXlncm91bmQudHN4) 0.00% 30 Missing ⚠️
...FeatureFlagsInLatestCompositionByFederatedGraph.ts 77.33% 17 Missing ⚠️
[...ionSlug]/[namespace]/graph/[slug]/schema/index.tsx](https://app.codecov.io/gh/wundergraph/cosmo/pull/2729?src=pr&el=tree&filepath=studio%2Fsrc%2Fpages%2F%5BorganizationSlug%5D%2F%5Bnamespace%5D%2Fgraph%2F%5Bslug%5D%2Fschema%2Findex.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wundergraph#diff-c3R1ZGlvL3NyYy9wYWdlcy9bb3JnYW5pemF0aW9uU2x1Z10vW25hbWVzcGFjZV0vZ3JhcGgvW3NsdWddL3NjaGVtYS9pbmRleC50c3g=) 0.00% 17 Missing ⚠️
[...ationSlug]/[namespace]/graph/[slug]/schema/sdl.tsx](https://app.codecov.io/gh/wundergraph/cosmo/pull/2729?src=pr&el=tree&filepath=studio%2Fsrc%2Fpages%2F%5BorganizationSlug%5D%2F%5Bnamespace%5D%2Fgraph%2F%5Bslug%5D%2Fschema%2Fsdl.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wundergraph#diff-c3R1ZGlvL3NyYy9wYWdlcy9bb3JnYW5pemF0aW9uU2x1Z10vW25hbWVzcGFjZV0vZ3JhcGgvW3NsdWddL3NjaGVtYS9zZGwudHN4) 0.00% 13 Missing ⚠️
...eature-flag/getFeatureSubgraphsByFederatedGraph.ts 86.30% 10 Missing ⚠️
...lplane/src/core/repositories/SubgraphRepository.ts 95.83% 5 Missing ⚠️
...ane/src/core/bufservices/workspace/getWorkspace.ts 0.00% 4 Missing ⚠️
...ane/src/core/repositories/FeatureFlagRepository.ts 98.05% 2 Missing ⚠️

❌ Your patch check has failed because the patch coverage (58.51%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff             @@
##             main    #2729       +/-   ##
===========================================
- Coverage   64.45%   40.44%   -24.01%     
===========================================
  Files         306      985      +679     
  Lines       43621   124092    +80471     
  Branches     4690     5639      +949     
===========================================
+ Hits        28114    50195    +22081     
- Misses      15485    72223    +56738     
- Partials       22     1674     +1652     
Files with missing lines Coverage Δ
...ntrolplane/src/core/bufservices/PlatformService.ts 82.62% <100.00%> (+0.19%) ⬆️
...ervices/federated-graph/getFederatedGraphByName.ts 89.36% <ø> (-1.92%) ⬇️
.../src/core/repositories/FederatedGraphRepository.ts 72.65% <100.00%> (ø)
studio/src/components/layout/graph-layout.tsx 0.00% <ø> (ø)
...ane/src/core/repositories/FeatureFlagRepository.ts 86.82% <98.05%> (-2.38%) ⬇️
...ane/src/core/bufservices/workspace/getWorkspace.ts 11.11% <0.00%> (+8.61%) ⬆️
...lplane/src/core/repositories/SubgraphRepository.ts 88.61% <95.83%> (+0.45%) ⬆️
...eature-flag/getFeatureSubgraphsByFederatedGraph.ts 86.30% <86.30%> (ø)
[...ationSlug]/[namespace]/graph/[slug]/schema/sdl.tsx](https://app.codecov.io/gh/wundergraph/cosmo/pull/2729?src=pr&el=tree&filepath=studio%2Fsrc%2Fpages%2F%5BorganizationSlug%5D%2F%5Bnamespace%5D%2Fgraph%2F%5Bslug%5D%2Fschema%2Fsdl.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=wundergraph#diff-c3R1ZGlvL3NyYy9wYWdlcy9bb3JnYW5pemF0aW9uU2x1Z10vW25hbWVzcGFjZV0vZ3JhcGgvW3NsdWddL3NjaGVtYS9zZGwudHN4) 0.00% <0.00%> (ø)
...FeatureFlagsInLatestCompositionByFederatedGraph.ts 77.33% <77.33%> (ø)
... and 4 more

... and 671 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 1, 2026

Router-nonroot image scan failed

❌ Security vulnerabilities found in image:

ghcr.io/wundergraph/cosmo/router:sha-74840b6869f6a18efbd25d558668f6de1daa8b98-nonroot

Please check the security vulnerabilities found in the PR.

If you believe this is a false positive, please add the vulnerability to the .trivyignore file and re-run the scan.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
controlplane/src/core/repositories/FeatureFlagRepository.ts (1)

495-512: Avoid 2N+1 lookups after paginating.

This path paginates in SQL, but each returned row still does byTargetId() and byId() serially. With the current 50-item cap, one page can still trigger 100+ extra DB queries. Folding the missing fields into the main query, or batch-loading them by ID, would keep the new endpoint’s latency predictable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controlplane/src/core/repositories/FeatureFlagRepository.ts` around lines 495
- 512, In FeatureFlagRepository where featureSubgraphTargets is iterated, avoid
N+1 by collecting all needed IDs (all targetId values and all baseSubgraphId
values from featureSubgraphTargets) and batch-loading them once (e.g., using
subgraphRepo.byIds or a new subgraphRepo.byTargetIds/byIds method) before the
loop; then map the returned subgraph rows by id/targetId and populate
featureSubgraphs from those maps (or alternatively fold the missing fields into
the primary SQL with joins) instead of calling subgraphRepo.byTargetId and
subgraphRepo.byId for each item.
controlplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.ts (1)

65-66: Assert the exact result set here.

This setup creates one eligible flag, but toBeGreaterThanOrEqual(1) plus some(...) still allows duplicates or unrelated extras. Tightening this to the exact returned IDs/names—or adding a second ineligible flag to the fixture—would actually lock down the “only flags in the latest composition” contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@controlplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.ts`
around lines 65 - 66, The test currently uses a loose assertion
(expect(resp.featureFlags.length).toBeGreaterThanOrEqual(1) plus some(...)), so
tighten it to assert the exact returned set: either change the length assertion
to expect(resp.featureFlags.length).toBe(1) and assert the single entry matches
flagName (e.g., resp.featureFlags[0].name === flagName) or replace both
assertions with a deterministic comparison of names/ids (e.g., compare
resp.featureFlags.map(f=>f.name) to [flagName]); alternatively, add a second
ineligible flag to the fixture and assert the response contains only the
eligible flag(s) to lock down the “only flags in the latest composition”
contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts`:
- Around line 65-84: The loop discards the composed schema-version id from
getFeatureFlagSchemaVersionsByBaseSchemaVersion (ffsInLatestValidComposition)
and re-reads the mutable current flag via featureFlagRepo.getFeatureFlagById,
causing drift; update the loop to load the exact composed flag version using the
schema-version identifier present on each ff (e.g., ff.schemaVersionId or
ff.featureFlagSchemaVersionId) and call the repository method that returns that
specific schema-version record (instead of getFeatureFlagById), then push that
version into featureFlags so the response reflects the flags as they were in the
latest composition.

In
`@controlplane/src/core/bufservices/feature-flag/getFeatureSubgraphsByFederatedGraph.ts`:
- Around line 70-87: The featureSubgraphs mapping in
getFeatureSubgraphsByFederatedGraph (inside the featureSubgraphs:
featureSubgraphs.map(...) block) omits createdUserId, causing inconsistent
payloads vs getFeatureSubgraphs; add createdUserId: fs.createdUserId to the
object returned by the map, and update any related type/interface (feature
subgraph DTO or return type) if necessary so createdUserId is included
throughout; keep other fields (e.g., routingURL, subscriptionProtocol,
websocketSubprotocol, type via convertToSubgraphType) unchanged.

In `@controlplane/src/core/repositories/FeatureFlagRepository.ts`:
- Around line 437-439: getFeatureSubgraphs currently restricts the query filter
to like(targets.name, `%${query}%`) which breaks "Search by ID or Name"; modify
the query handling so the conditions array includes an OR branch that also
checks subgraphs.id for an exact match when query is a UUID (or always include
equality on subgraphs.id alongside the name like). Locate the code that pushes
like(targets.name, `%${query}%`) and add a complementary condition that checks
subgraphs.id (use equals/subgraphs.id === query or the repository's equality
helper) so searches by feature-subgraph ID continue to return results.

In
`@controlplane/test/feature-flag/get-feature-subgraphs-by-federated-graph.test.ts`:
- Around line 313-323: Replace the loose .some() check with a strict cardinality
assertion: for the call to client.getFeatureSubgraphsByFederatedGraph in this
test, verify that resp.featureSubgraphs contains exactly one entry named
"users-feature" (e.g., count/filter the entries with s.name === 'users-feature'
and expect the count to equal 1) and optionally assert that
resp.featureSubgraphs has no duplicate names by comparing its length to the
length of the set/unique list of names; update the test that references
resp.featureSubgraphs to enforce these exact-unique expectations.

In `@studio/src/pages/`[organizationSlug]/[namespace]/graph/[slug]/playground.tsx:
- Around line 745-763: Extract the feature-flag name resolution into a new
constant (e.g., selectedFeatureFlagName) by reading
compositionFlagsData?.featureFlags and finding the flag with id ===
loadSchemaGraphId, and then modify the useQuery call for
getFederatedGraphSDLByName to include an enabled condition so it only runs when
either type !== 'featureFlag' or (type === 'featureFlag' && compositionFlagsData
has resolved and selectedFeatureFlagName is defined); update the query's
featureFlagName to use selectedFeatureFlagName so the SDL query does not fire
with undefined and avoids the unnecessary re-fetch/flicker.

---

Nitpick comments:
In `@controlplane/src/core/repositories/FeatureFlagRepository.ts`:
- Around line 495-512: In FeatureFlagRepository where featureSubgraphTargets is
iterated, avoid N+1 by collecting all needed IDs (all targetId values and all
baseSubgraphId values from featureSubgraphTargets) and batch-loading them once
(e.g., using subgraphRepo.byIds or a new subgraphRepo.byTargetIds/byIds method)
before the loop; then map the returned subgraph rows by id/targetId and populate
featureSubgraphs from those maps (or alternatively fold the missing fields into
the primary SQL with joins) instead of calling subgraphRepo.byTargetId and
subgraphRepo.byId for each item.

In
`@controlplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.ts`:
- Around line 65-66: The test currently uses a loose assertion
(expect(resp.featureFlags.length).toBeGreaterThanOrEqual(1) plus some(...)), so
tighten it to assert the exact returned set: either change the length assertion
to expect(resp.featureFlags.length).toBe(1) and assert the single entry matches
flagName (e.g., resp.featureFlags[0].name === flagName) or replace both
assertions with a deterministic comparison of names/ids (e.g., compare
resp.featureFlags.map(f=>f.name) to [flagName]); alternatively, add a second
ineligible flag to the fixture and assert the response contains only the
eligible flag(s) to lock down the “only flags in the latest composition”
contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a8ab9dd9-6e5d-4ab3-ab4c-328fa99f892d

📥 Commits

Reviewing files that changed from the base of the PR and between fe662bf and c5181a5.

⛔ Files ignored due to path filters (2)
  • connect-go/gen/proto/wg/cosmo/platform/v1/platform.pb.go is excluded by !**/*.pb.go, !**/gen/**
  • connect-go/gen/proto/wg/cosmo/platform/v1/platformv1connect/platform.connect.go is excluded by !**/gen/**
📒 Files selected for processing (16)
  • connect/src/wg/cosmo/platform/v1/platform-PlatformService_connectquery.ts
  • connect/src/wg/cosmo/platform/v1/platform_connect.ts
  • connect/src/wg/cosmo/platform/v1/platform_pb.ts
  • controlplane/src/core/bufservices/PlatformService.ts
  • controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts
  • controlplane/src/core/bufservices/feature-flag/getFeatureSubgraphsByFederatedGraph.ts
  • controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.ts
  • controlplane/src/core/repositories/FeatureFlagRepository.ts
  • controlplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.ts
  • controlplane/test/feature-flag/get-feature-subgraphs-by-federated-graph.test.ts
  • proto/wg/cosmo/platform/v1/platform.proto
  • studio/src/components/layout/graph-layout.tsx
  • studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx
  • studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/schema/index.tsx
  • studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/schema/sdl.tsx
  • studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/subgraphs.tsx
💤 Files with no reviewable changes (2)
  • studio/src/components/layout/graph-layout.tsx
  • controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.ts

Comment on lines +65 to +84
// Get feature flag IDs from the latest valid composition
const ffsInLatestValidComposition = await featureFlagRepo.getFeatureFlagSchemaVersionsByBaseSchemaVersion({
baseSchemaVersionId: federatedGraph.schemaVersionId,
});

const featureFlags: FeatureFlagDTO[] = [];
if (ffsInLatestValidComposition) {
for (const ff of ffsInLatestValidComposition) {
if (!ff.featureFlagId) {
continue;
}
const flag = await featureFlagRepo.getFeatureFlagById({
featureFlagId: ff.featureFlagId,
namespaceId: namespace.id,
includeSubgraphs: false,
});
if (flag) {
featureFlags.push(flag);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This returns the current flag record, not the flag version in the latest composition.

getFeatureFlagSchemaVersionsByBaseSchemaVersion() already tells us which schema-version rows were composed, but the loop throws that version id away and reloads by mutable featureFlagId. If a flag is edited or deleted after the last valid composition, this endpoint will drift from what was actually composed—or omit the flag entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts`
around lines 65 - 84, The loop discards the composed schema-version id from
getFeatureFlagSchemaVersionsByBaseSchemaVersion (ffsInLatestValidComposition)
and re-reads the mutable current flag via featureFlagRepo.getFeatureFlagById,
causing drift; update the loop to load the exact composed flag version using the
schema-version identifier present on each ff (e.g., ff.schemaVersionId or
ff.featureFlagSchemaVersionId) and call the repository method that returns that
specific schema-version record (instead of getFeatureFlagById), then push that
version into featureFlags so the response reflects the flags as they were in the
latest composition.

Comment on lines +70 to +87
featureSubgraphs: featureSubgraphs.map((fs) => ({
id: fs.id,
name: fs.name,
routingURL: fs.routingUrl,
lastUpdatedAt: fs.lastUpdatedAt,
labels: fs.labels,
targetId: fs.targetId,
subscriptionUrl: fs.subscriptionUrl,
namespace: fs.namespace,
subscriptionProtocol: fs.subscriptionProtocol,
isEventDrivenGraph: fs.isEventDrivenGraph,
isV2Graph: fs.isV2Graph,
websocketSubprotocol: fs.websocketSubprotocol || '',
isFeatureSubgraph: fs.isFeatureSubgraph,
baseSubgraphId: fs.baseSubgraphId,
baseSubgraphName: fs.baseSubgraphName,
type: convertToSubgraphType(fs.type),
})),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Populate createdUserId in this mapping too.

getFeatureSubgraphs returns the same feature-subgraph payload with createdUserId, but this handler drops it. After the UI switches to this RPC, consumers will get a partially populated featureSubgraph object compared to the existing endpoint.

Proposed fix
       featureSubgraphs: featureSubgraphs.map((fs) => ({
         id: fs.id,
         name: fs.name,
         routingURL: fs.routingUrl,
         lastUpdatedAt: fs.lastUpdatedAt,
         labels: fs.labels,
+        createdUserId: fs.creatorUserId,
         targetId: fs.targetId,
         subscriptionUrl: fs.subscriptionUrl,
         namespace: fs.namespace,
         subscriptionProtocol: fs.subscriptionProtocol,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
featureSubgraphs: featureSubgraphs.map((fs) => ({
id: fs.id,
name: fs.name,
routingURL: fs.routingUrl,
lastUpdatedAt: fs.lastUpdatedAt,
labels: fs.labels,
targetId: fs.targetId,
subscriptionUrl: fs.subscriptionUrl,
namespace: fs.namespace,
subscriptionProtocol: fs.subscriptionProtocol,
isEventDrivenGraph: fs.isEventDrivenGraph,
isV2Graph: fs.isV2Graph,
websocketSubprotocol: fs.websocketSubprotocol || '',
isFeatureSubgraph: fs.isFeatureSubgraph,
baseSubgraphId: fs.baseSubgraphId,
baseSubgraphName: fs.baseSubgraphName,
type: convertToSubgraphType(fs.type),
})),
featureSubgraphs: featureSubgraphs.map((fs) => ({
id: fs.id,
name: fs.name,
routingURL: fs.routingUrl,
lastUpdatedAt: fs.lastUpdatedAt,
labels: fs.labels,
createdUserId: fs.creatorUserId,
targetId: fs.targetId,
subscriptionUrl: fs.subscriptionUrl,
namespace: fs.namespace,
subscriptionProtocol: fs.subscriptionProtocol,
isEventDrivenGraph: fs.isEventDrivenGraph,
isV2Graph: fs.isV2Graph,
websocketSubprotocol: fs.websocketSubprotocol || '',
isFeatureSubgraph: fs.isFeatureSubgraph,
baseSubgraphId: fs.baseSubgraphId,
baseSubgraphName: fs.baseSubgraphName,
type: convertToSubgraphType(fs.type),
})),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@controlplane/src/core/bufservices/feature-flag/getFeatureSubgraphsByFederatedGraph.ts`
around lines 70 - 87, The featureSubgraphs mapping in
getFeatureSubgraphsByFederatedGraph (inside the featureSubgraphs:
featureSubgraphs.map(...) block) omits createdUserId, causing inconsistent
payloads vs getFeatureSubgraphs; add createdUserId: fs.createdUserId to the
object returned by the map, and update any related type/interface (feature
subgraph DTO or return type) if necessary so createdUserId is included
throughout; keep other fields (e.g., routingURL, subscriptionProtocol,
websocketSubprotocol, type via convertToSubgraphType) unchanged.

Comment on lines +745 to 763
const { data: compositionFlagsData } = useQuery(
getFeatureFlagsInLatestCompositionByFederatedGraph,
{
federatedGraphName: graphContext?.graph?.name,
namespace: graphContext?.graph?.namespace,
},
{
enabled: !!graphContext?.graph?.name,
},
);

const { data, isLoading: isLoadingGraphSchema } = useQuery(getFederatedGraphSDLByName, {
name: graphContext?.graph?.name,
namespace: graphContext?.graph?.namespace,
featureFlagName:
type === 'featureFlag'
? graphContext?.featureFlagsInLatestValidComposition.find((f) => f.id === loadSchemaGraphId)?.name
? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name
: undefined,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "playground.tsx" | grep -E "graph/\[slug\]"

Repository: wundergraph/cosmo

Length of output: 138


🏁 Script executed:

cat studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx | sed -n '730,770p'

Repository: wundergraph/cosmo

Length of output: 1469


🏁 Script executed:

# Get broader context to understand compositionFlagsData and query setup
cat studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx | sed -n '735,800p'

Repository: wundergraph/cosmo

Length of output: 2329


🏁 Script executed:

# Search for usage of compositionFlagsData and featureFlagName in the file
rg "compositionFlagsData|featureFlagName" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -n

Repository: wundergraph/cosmo

Length of output: 767


🏁 Script executed:

# Check the useQuery hook implementation to understand how enabled works
rg "export.*useQuery" studio/src --type ts --type tsx -A 5 | head -50

Repository: wundergraph/cosmo

Length of output: 88


🏁 Script executed:

# Look for imports to understand the query library being used
head -50 studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx | grep -E "^import|^from"

Repository: wundergraph/cosmo

Length of output: 1837


🏁 Script executed:

# Check the subgraphData query to see how it uses enabled condition
sed -n '765,780p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx

Repository: wundergraph/cosmo

Length of output: 680


🏁 Script executed:

# Look for all useQuery calls to understand the pattern
rg "useQuery\(" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -B 1 -A 8

Repository: wundergraph/cosmo

Length of output: 1535


🏁 Script executed:

# Check if there are any test files or integration tests related to feature flags
find . -type f \( -name "*.test.tsx" -o -name "*.spec.tsx" \) | xargs grep -l "featureFlag\|compositionFlags" 2>/dev/null | head -5

Repository: wundergraph/cosmo

Length of output: 43


🏁 Script executed:

# Check the actual behavior of useQuery - look for how it handles missing enabled condition
# and understand if queries fire immediately
rg "enabled:" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -B 2 -A 1

Repository: wundergraph/cosmo

Length of output: 322


🏁 Script executed:

# Look at the downstream usage of featureFlagName in the fetcher/request headers
sed -n '185,230p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx

Repository: wundergraph/cosmo

Length of output: 1271


🏁 Script executed:

# Check if there's any dependency array or effect logic that might prevent the race condition
sed -n '800,850p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx

Repository: wundergraph/cosmo

Length of output: 1523


🏁 Script executed:

# Look at where graphiQLFetch is called to understand how featureFlagName flows through
rg "graphiQLFetch\(" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -B 2 -A 10

Repository: wundergraph/cosmo

Length of output: 527


🏁 Script executed:

# Check if there's any caching mechanism that might prevent the double-fetch issue
rg "createGraphiQLFetcher" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -B 5 -A 20

Repository: wundergraph/cosmo

Length of output: 2178


🏁 Script executed:

# Understand the exact moment when compositionFlagsData is available
# Check if there's any dependency on type or loadSchemaGraphId in the composition flags query
sed -n '745,754p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx

Repository: wundergraph/cosmo

Length of output: 344


🏁 Script executed:

# Check the fetcher dependency array to see if it includes compositionFlagsData
rg "createGraphiQLFetcher" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -A 25 | grep -A 20 "^\]"

Repository: wundergraph/cosmo

Length of output: 43


🏁 Script executed:

# Verify the exact location of the second feature flag lookup mentioned in the comment
sed -n '1005,1025p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx

Repository: wundergraph/cosmo

Length of output: 662


🏁 Script executed:

# Check the structure of useQuery to confirm it supports the three-argument pattern with options
rg "function useQuery|export.*useQuery" studio/src -A 15 | head -80

Repository: wundergraph/cosmo

Length of output: 43


🏁 Script executed:

# Verify if there are other similar patterns where enabled gate is used for dependent queries
rg "enabled:.*\|\|.*\?" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx

Repository: wundergraph/cosmo

Length of output: 43


🏁 Script executed:

# Check how the createGraphiQLFetcher is used and if schema changes when compositionFlagsData arrives
sed -n '980,1030p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx

Repository: wundergraph/cosmo

Length of output: 1538


🏁 Script executed:

# Check the connect-query version and documentation reference
rg "@connectrpc/connect-query" studio/package.json

Repository: wundergraph/cosmo

Length of output: 103


🏁 Script executed:

# Look at how compositionFlagsData is used and when it's expected to be available
sed -n '745,800p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx

Repository: wundergraph/cosmo

Length of output: 1939


🏁 Script executed:

# Verify the exact scenario: does the SDL query have compositionFlagsData in its dependency tracking?
rg "getFederatedGraphSDLByName" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx -B 2 -A 12

Repository: wundergraph/cosmo

Length of output: 1369


🏁 Script executed:

# Check if there's any query key hashing or dependency tracking in connect-query
# Look at how parameters affect query caching
web_search "connectrpc connect-query useQuery enabled option documentation"

Repository: wundergraph/cosmo

Length of output: 111


🏁 Script executed:

# Look more carefully at when the schema variable (used in fetcher) updates
# Check the useMemo dependency for schema
sed -n '775,790p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx

Repository: wundergraph/cosmo

Length of output: 666


🏁 Script executed:

# Verify the complete pattern of how data flows from SDL query to fetcher to GraphiQL
sed -n '1000,1015p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx

Repository: wundergraph/cosmo

Length of output: 569


🏁 Script executed:

# Let's verify the actual runtime behavior by checking if there are any tests or examples
# that show this scenario
rg "type === 'featureFlag'" studio/src -B 3 -A 3

Repository: wundergraph/cosmo

Length of output: 4208


🏁 Script executed:

# Check if there's any stale-while-revalidate or background revalidation that might mask the issue
rg "staleTime|cacheTime|gcTime" studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx

Repository: wundergraph/cosmo

Length of output: 43


🏁 Script executed:

# Get the complete useQuery call for SDL to verify there's no enabled option present
sed -n '756,764p' studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx

Repository: wundergraph/cosmo

Length of output: 412


Wait for the selected feature flag before loading SDL.

When type === 'featureFlag', the SDL query derives featureFlagName from compositionFlagsData, but that query loads independently and completes asynchronously. Without gating, getFederatedGraphSDLByName fires immediately on first render with featureFlagName: undefined, hydrating GraphiQL with the base graph schema. Once compositionFlagsData resolves and the flag name becomes available, the query key changes and fires again, causing an unnecessary re-fetch and schema flicker. Extract the flag name lookup into selectedFeatureFlagName and add an enabled condition to prevent the SDL query from running until the flag is resolved.

💡 Proposed fix
+  const selectedFeatureFlagName =
+    type === 'featureFlag'
+      ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name
+      : undefined;
+
-  const { data, isLoading: isLoadingGraphSchema } = useQuery(getFederatedGraphSDLByName, {
-    name: graphContext?.graph?.name,
-    namespace: graphContext?.graph?.namespace,
-    featureFlagName:
-      type === 'featureFlag'
-        ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name
-        : undefined,
-  });
+  const { data, isLoading: isLoadingGraphSchema } = useQuery(
+    getFederatedGraphSDLByName,
+    {
+      name: graphContext?.graph?.name,
+      namespace: graphContext?.graph?.namespace,
+      featureFlagName: selectedFeatureFlagName,
+    },
+    {
+      enabled: !!graphContext?.graph?.name && (type !== 'featureFlag' || !!selectedFeatureFlagName),
+    },
+  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { data: compositionFlagsData } = useQuery(
getFeatureFlagsInLatestCompositionByFederatedGraph,
{
federatedGraphName: graphContext?.graph?.name,
namespace: graphContext?.graph?.namespace,
},
{
enabled: !!graphContext?.graph?.name,
},
);
const { data, isLoading: isLoadingGraphSchema } = useQuery(getFederatedGraphSDLByName, {
name: graphContext?.graph?.name,
namespace: graphContext?.graph?.namespace,
featureFlagName:
type === 'featureFlag'
? graphContext?.featureFlagsInLatestValidComposition.find((f) => f.id === loadSchemaGraphId)?.name
? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name
: undefined,
});
const { data: compositionFlagsData } = useQuery(
getFeatureFlagsInLatestCompositionByFederatedGraph,
{
federatedGraphName: graphContext?.graph?.name,
namespace: graphContext?.graph?.namespace,
},
{
enabled: !!graphContext?.graph?.name,
},
);
const selectedFeatureFlagName =
type === 'featureFlag'
? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name
: undefined;
const { data, isLoading: isLoadingGraphSchema } = useQuery(
getFederatedGraphSDLByName,
{
name: graphContext?.graph?.name,
namespace: graphContext?.graph?.namespace,
featureFlagName: selectedFeatureFlagName,
},
{
enabled: !!graphContext?.graph?.name && (type !== 'featureFlag' || !!selectedFeatureFlagName),
},
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@studio/src/pages/`[organizationSlug]/[namespace]/graph/[slug]/playground.tsx
around lines 745 - 763, Extract the feature-flag name resolution into a new
constant (e.g., selectedFeatureFlagName) by reading
compositionFlagsData?.featureFlags and finding the flag with id ===
loadSchemaGraphId, and then modify the useQuery call for
getFederatedGraphSDLByName to include an enabled condition so it only runs when
either type !== 'featureFlag' or (type === 'featureFlag' && compositionFlagsData
has resolved and selectedFeatureFlagName is defined); update the query's
featureFlagName to use selectedFeatureFlagName so the SDL query does not fire
with undefined and avoids the unnecessary re-fetch/flicker.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx (1)

745-763: ⚠️ Potential issue | 🟠 Major

Prevent feature-flag mode from loading SDL before flag resolution.

Line 756 still executes the SDL query before the selected flag name exists (Line 761), so feature-flag mode can fetch base schema first and re-fetch after flags load. Also, the flag lookup is duplicated in three places (Line 761, Line 1012, Line 1067), which risks drift.

💡 Proposed fix
   const { data: compositionFlagsData } = useQuery(
     getFeatureFlagsInLatestCompositionByFederatedGraph,
@@
     },
   );
+  const selectedFeatureFlagName =
+    type === 'featureFlag'
+      ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name
+      : undefined;
 
-  const { data, isLoading: isLoadingGraphSchema } = useQuery(getFederatedGraphSDLByName, {
-    name: graphContext?.graph?.name,
-    namespace: graphContext?.graph?.namespace,
-    featureFlagName:
-      type === 'featureFlag'
-        ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name
-        : undefined,
-  });
+  const { data, isLoading: isLoadingGraphSchema } = useQuery(
+    getFederatedGraphSDLByName,
+    {
+      name: graphContext?.graph?.name,
+      namespace: graphContext?.graph?.namespace,
+      featureFlagName: selectedFeatureFlagName,
+    },
+    {
+      enabled: !!graphContext?.graph?.name && (type !== 'featureFlag' || !!selectedFeatureFlagName),
+    },
+  );
@@
           graphContext?.graph?.id || '',
-          type === 'featureFlag'
-            ? (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId)?.name
-            : undefined,
+          selectedFeatureFlagName,
         ),
@@
-        if (type === 'featureFlag') {
-          const featureFlag = (compositionFlagsData?.featureFlags ?? []).find((f) => f.id === loadSchemaGraphId);
-          if (featureFlag) {
-            requestHeaders['X-Feature-Flag'] = featureFlag.name;
-          }
+        if (selectedFeatureFlagName) {
+          requestHeaders['X-Feature-Flag'] = selectedFeatureFlagName;
         }
@@
-    compositionFlagsData?.featureFlags,
+    selectedFeatureFlagName,
@@
-    compositionFlagsData?.featureFlags,
+    selectedFeatureFlagName,

Also applies to: 1011-1013, 1066-1070

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@studio/src/pages/`[organizationSlug]/[namespace]/graph/[slug]/playground.tsx
around lines 745 - 763, Prevent the SDL query from running in feature-flag mode
until the flags are loaded and the selected flag is resolved: compute a single
selectedFlagName variable by deriving it once from compositionFlagsData,
loadSchemaGraphId and use that name wherever needed (replace the repeated .find
usages at the three spots), and pass featureFlagName: selectedFlagName to the
getFederatedGraphSDLByName useQuery; also set the useQuery enabled flag to only
run when either type !== 'featureFlag' or selectedFlagName is truthy so the base
SDL is not fetched prematurely.
controlplane/src/core/repositories/FeatureFlagRepository.ts (1)

437-439: ⚠️ Potential issue | 🟡 Minor

Keep feature-subgraph ID search working.

This new path only filters on targets.name, so UUID searches that already work in getFeatureSubgraphs() stop returning results here.

💡 Proposed fix
     if (query) {
-      conditions.push(like(targets.name, `%${query}%`));
+      conditions.push(isValidUuid(query) ? eq(subgraphs.id, query) : like(targets.name, `%${query}%`));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controlplane/src/core/repositories/FeatureFlagRepository.ts` around lines 437
- 439, The current query filter only matches targets.name so UUID searches stop
returning results; in the method that builds conditions (where you have if
(query) { conditions.push(like(targets.name, `%${query}%`)); }) update it to
also check the feature-subgraph id—e.g., add a second condition on targets.id
(exact match) or OR a like on targets.id when query looks like a UUID—so
getFeatureSubgraphs()/the same query path will return results for ID searches as
well; modify the conditions.push call(s) to include targets.id alongside
targets.name (and optionally detect UUID format to prefer exact match).
controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts (1)

69-83: ⚠️ Potential issue | 🟠 Major

This returns current flag state, not the latest composed snapshot.

getFeatureFlagSchemaVersionsByBaseSchemaVersion() gives you the persisted composition membership, but this loop throws that record away and reloads the mutable live flag by featureFlagId. If a flag is disabled, relabeled, or deleted after that composition, this RPC will drift from what the latest composition actually contained.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts`
around lines 69 - 83, The code currently discards the composition membership
returned by getFeatureFlagSchemaVersionsByBaseSchemaVersion
(ffsInLatestValidComposition) and reloads live flags via
featureFlagRepo.getFeatureFlagById, which returns the current mutable state
instead of the snapshot that was part of the composition; change the lookup to
fetch the versioned/snapshot data tied to the composition record (e.g., use the
schema-version-scoped accessor instead of getFeatureFlagById) — for example call
a repository method that returns the feature-flag snapshot by featureFlagId +
base/schema version (or use the data already present on the ff record) so you
populate featureFlags from the composed snapshot (reference symbols:
getFeatureFlagSchemaVersionsByBaseSchemaVersion, ffsInLatestValidComposition,
ff.featureFlagId, featureFlagRepo.getFeatureFlagById, featureFlags).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controlplane/src/core/repositories/FeatureFlagRepository.ts`:
- Around line 467-484: The ORDER BY in FeatureFlagRepository's query uses only
targets.createdAt (baseQuery.orderBy(asc(targets.createdAt))) which is
non-deterministic for identical timestamps; update the ordering to include a
stable tiebreaker (e.g., secondary sort by targets.id or
subgraphsToFederatedGraph.id) so pagination with limit/offset is deterministic,
and keep the count query as-is; locate where baseQuery.orderBy(...) is called
and add the secondary asc(...) tiebreaker to the same orderBy chain.

In `@studio/src/pages/`[organizationSlug]/[namespace]/graph/[slug]/playground.tsx:
- Around line 619-629: The query for composition flags is being enabled only by
graphContext?.graph?.name but reads router.query.namespace into the namespace
const without guarding for its availability, which can fire a request with a
missing namespace during router hydration; update how namespace is derived
(allow undefined) and change the useQuery enabled option for
getFeatureFlagsInLatestCompositionByFederatedGraph to require both
graphContext?.graph?.name and namespace (e.g., enabled:
!!graphContext?.graph?.name && !!namespace) so the query only runs when
namespace is present.

---

Duplicate comments:
In
`@controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts`:
- Around line 69-83: The code currently discards the composition membership
returned by getFeatureFlagSchemaVersionsByBaseSchemaVersion
(ffsInLatestValidComposition) and reloads live flags via
featureFlagRepo.getFeatureFlagById, which returns the current mutable state
instead of the snapshot that was part of the composition; change the lookup to
fetch the versioned/snapshot data tied to the composition record (e.g., use the
schema-version-scoped accessor instead of getFeatureFlagById) — for example call
a repository method that returns the feature-flag snapshot by featureFlagId +
base/schema version (or use the data already present on the ff record) so you
populate featureFlags from the composed snapshot (reference symbols:
getFeatureFlagSchemaVersionsByBaseSchemaVersion, ffsInLatestValidComposition,
ff.featureFlagId, featureFlagRepo.getFeatureFlagById, featureFlags).

In `@controlplane/src/core/repositories/FeatureFlagRepository.ts`:
- Around line 437-439: The current query filter only matches targets.name so
UUID searches stop returning results; in the method that builds conditions
(where you have if (query) { conditions.push(like(targets.name, `%${query}%`));
}) update it to also check the feature-subgraph id—e.g., add a second condition
on targets.id (exact match) or OR a like on targets.id when query looks like a
UUID—so getFeatureSubgraphs()/the same query path will return results for ID
searches as well; modify the conditions.push call(s) to include targets.id
alongside targets.name (and optionally detect UUID format to prefer exact
match).

In `@studio/src/pages/`[organizationSlug]/[namespace]/graph/[slug]/playground.tsx:
- Around line 745-763: Prevent the SDL query from running in feature-flag mode
until the flags are loaded and the selected flag is resolved: compute a single
selectedFlagName variable by deriving it once from compositionFlagsData,
loadSchemaGraphId and use that name wherever needed (replace the repeated .find
usages at the three spots), and pass featureFlagName: selectedFlagName to the
getFederatedGraphSDLByName useQuery; also set the useQuery enabled flag to only
run when either type !== 'featureFlag' or selectedFlagName is truthy so the base
SDL is not fetched prematurely.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 244afbfd-1f02-4c3d-bc07-467895044470

📥 Commits

Reviewing files that changed from the base of the PR and between c5181a5 and 41890f4.

📒 Files selected for processing (5)
  • controlplane/src/core/bufservices/feature-flag/getFeatureFlagsInLatestCompositionByFederatedGraph.ts
  • controlplane/src/core/repositories/FeatureFlagRepository.ts
  • controlplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.ts
  • controlplane/test/feature-flag/get-feature-subgraphs-by-federated-graph.test.ts
  • studio/src/pages/[organizationSlug]/[namespace]/graph/[slug]/playground.tsx
✅ Files skipped from review due to trivial changes (2)
  • controlplane/test/feature-flag/get-feature-flags-in-latest-composition-by-federated-graph.test.ts
  • controlplane/test/feature-flag/get-feature-subgraphs-by-federated-graph.test.ts

Comment on lines +467 to +484
.where(and(...conditions, eq(subgraphsToFederatedGraph.federatedGraphId, federatedGraphId)))
.orderBy(asc(targets.createdAt));

// Get total count
const countResult = await this.db
.select({ count: count() })
.from(baseQuery.as('feature_subgraphs_by_fed_graph'))
.execute();

const totalCount = countResult[0]?.count ?? 0;

// Apply pagination
if (limit > 0) {
baseQuery.limit(limit);
}
if (offset > 0) {
baseQuery.offset(offset);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a stable tiebreaker to the paginated ordering.

ORDER BY targets.createdAt alone is not deterministic when multiple rows share the same timestamp, so offset-based paging can duplicate or skip feature subgraphs between pages.

💡 Proposed fix
       .innerJoin(namespaces, eq(namespaces.id, targets.namespaceId))
       .where(and(...conditions, eq(subgraphsToFederatedGraph.federatedGraphId, federatedGraphId)))
-      .orderBy(asc(targets.createdAt));
+      .orderBy(asc(targets.createdAt), asc(subgraphs.id));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.where(and(...conditions, eq(subgraphsToFederatedGraph.federatedGraphId, federatedGraphId)))
.orderBy(asc(targets.createdAt));
// Get total count
const countResult = await this.db
.select({ count: count() })
.from(baseQuery.as('feature_subgraphs_by_fed_graph'))
.execute();
const totalCount = countResult[0]?.count ?? 0;
// Apply pagination
if (limit > 0) {
baseQuery.limit(limit);
}
if (offset > 0) {
baseQuery.offset(offset);
}
.where(and(...conditions, eq(subgraphsToFederatedGraph.federatedGraphId, federatedGraphId)))
.orderBy(asc(targets.createdAt), asc(subgraphs.id));
// Get total count
const countResult = await this.db
.select({ count: count() })
.from(baseQuery.as('feature_subgraphs_by_fed_graph'))
.execute();
const totalCount = countResult[0]?.count ?? 0;
// Apply pagination
if (limit > 0) {
baseQuery.limit(limit);
}
if (offset > 0) {
baseQuery.offset(offset);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controlplane/src/core/repositories/FeatureFlagRepository.ts` around lines 467
- 484, The ORDER BY in FeatureFlagRepository's query uses only targets.createdAt
(baseQuery.orderBy(asc(targets.createdAt))) which is non-deterministic for
identical timestamps; update the ordering to include a stable tiebreaker (e.g.,
secondary sort by targets.id or subgraphsToFederatedGraph.id) so pagination with
limit/offset is deterministic, and keep the count query as-is; locate where
baseQuery.orderBy(...) is called and add the secondary asc(...) tiebreaker to
the same orderBy chain.

Comment on lines +619 to +629
const namespace = router.query.namespace as string;

const { data: compositionFlagsData } = useQuery(
getFeatureFlagsInLatestCompositionByFederatedGraph,
{
federatedGraphName: graphContext?.graph?.name,
namespace,
},
{
enabled: !!graphContext?.graph?.name,
},
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard the composition-flags query on namespace availability.

Line 619 reads router.query.namespace directly, but Line 628 enables the query only by graph name. During initial router hydration this can issue a request with missing namespace.

💡 Proposed fix
-  const namespace = router.query.namespace as string;
+  const namespace =
+    (router.query.namespace as string | undefined) ?? graphContext?.graph?.namespace ?? '';
@@
     {
-      enabled: !!graphContext?.graph?.name,
+      enabled: !!graphContext?.graph?.name && !!namespace,
     },
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@studio/src/pages/`[organizationSlug]/[namespace]/graph/[slug]/playground.tsx
around lines 619 - 629, The query for composition flags is being enabled only by
graphContext?.graph?.name but reads router.query.namespace into the namespace
const without guarding for its availability, which can fire a request with a
missing namespace during router hydration; update how namespace is derived
(allow undefined) and change the useQuery enabled option for
getFeatureFlagsInLatestCompositionByFederatedGraph to require both
graphContext?.graph?.name and namespace (e.g., enabled:
!!graphContext?.graph?.name && !!namespace) so the query only runs when
namespace is present.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
controlplane/src/core/repositories/FederatedGraphRepository.ts (1)

487-490: ⚠️ Potential issue | 🟠 Major

Use schema.targets.namespaceId in the mixed RBAC branch.

Line 489 compares namespace IDs against schema.targets.id, which will not match. Actors with both namespace-scoped and resource-scoped graph access lose the namespace portion of their permissions, causing list() to under-return graphs they should access.

Suggested change
       conditions.push(
-        or(inArray(schema.targets.id, [...new Set(namespaces)]), inArray(schema.targets.id, [...new Set(resources)])),
+        or(
+          inArray(schema.targets.namespaceId, [...new Set(namespaces)]),
+          inArray(schema.targets.id, [...new Set(resources)]),
+        ),
       );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controlplane/src/core/repositories/FederatedGraphRepository.ts` around lines
487 - 490, In FederatedGraphRepository (the mixed RBAC branch inside the
list/filter construction), change the left-hand field used for namespace checks
from schema.targets.id to schema.targets.namespaceId so namespace-scoped
permissions are matched correctly; specifically, update the or(...) call that
currently does or(inArray(schema.targets.id, [...new Set(namespaces)]),
inArray(schema.targets.id, [...new Set(resources)])) to use
schema.targets.namespaceId for the namespaces side while keeping
schema.targets.id for resource-scoped checks and the existing de-duplication
with new Set.
♻️ Duplicate comments (1)
controlplane/src/core/repositories/FeatureFlagRepository.ts (1)

467-468: 🛠️ Refactor suggestion | 🟠 Major

Add a stable tiebreaker to the paginated ordering.

ORDER BY targets.createdAt alone is not deterministic when multiple rows share the same timestamp, so offset-based paging can duplicate or skip feature subgraphs between pages.

💡 Proposed fix
       .innerJoin(namespaces, eq(namespaces.id, targets.namespaceId))
       .where(and(...conditions, eq(subgraphsToFederatedGraph.federatedGraphId, federatedGraphId)))
-      .orderBy(asc(targets.createdAt));
+      .orderBy(asc(targets.createdAt), asc(subgraphs.id));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controlplane/src/core/repositories/FeatureFlagRepository.ts` around lines 467
- 468, The ORDER BY in FeatureFlagRepository using
.orderBy(asc(targets.createdAt)) is not deterministic and can cause
duplicates/skips during offset paging; update the query that builds the ordering
(the call chaining ending with .orderBy(...)) to add a stable tiebreaker such as
the primary key (e.g., targets.id or targets.pk) and include it with the same
sort direction (e.g., .orderBy(asc(targets.createdAt), asc(targets.id))) so rows
with identical createdAt values are consistently ordered.
🧹 Nitpick comments (3)
controlplane/src/core/repositories/FeatureFlagRepository.ts (2)

251-253: Redundant condition can be simplified.

Since isOrganizationViewer is defined as isOrganizationAdminOrDeveloper || this.roles.includes('organization-viewer') (per RBACEvaluator.ts:80), checking isOrganizationAdminOrDeveloper separately is redundant.

💡 Proposed simplification
   private applyRbacConditionsToQuery(rbac: RBACEvaluator | undefined, conditions: (SQL<unknown> | undefined)[]) {
-    if (!rbac || rbac.isOrganizationAdminOrDeveloper || rbac.isOrganizationViewer) {
+    if (!rbac || rbac.isOrganizationViewer) {
       return true;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controlplane/src/core/repositories/FeatureFlagRepository.ts` around lines 251
- 253, The condition in FeatureFlagRepository (variable rbac) redundantly checks
rbac.isOrganizationAdminOrDeveloper and rbac.isOrganizationViewer; since
RBACEvaluator's isOrganizationViewer already includes
isOrganizationAdminOrDeveloper, simplify the guard to only check for absence of
rbac or rbac.isOrganizationViewer (i.e., replace the current if (!rbac ||
rbac.isOrganizationAdminOrDeveloper || rbac.isOrganizationViewer) return true;
with if (!rbac || rbac.isOrganizationViewer) return true;), referencing the
RBACEvaluator properties to justify the change.

489-505: N+1 query pattern could be optimized with batch fetching.

Each iteration executes two separate DB queries (byTargetId and byId). With a page of N results, this generates 2N additional queries. Consider collecting all targetIds and baseSubgraphIds upfront and fetching them in bulk.

💡 Proposed batch approach
     const featureSubgraphTargets = await baseQuery.execute();
 
+    // Batch fetch all required data
+    const targetIds = featureSubgraphTargets.map((f) => f.targetId);
+    const baseSubgraphIds = [...new Set(featureSubgraphTargets.map((f) => f.baseSubgraphId))];
+    
+    const [subgraphsByTarget, baseSubgraphsById] = await Promise.all([
+      subgraphRepo.byTargetIds(targetIds),  // Would need to add this batch method
+      subgraphRepo.byIds(baseSubgraphIds),  // Would need to add this batch method
+    ]);
+    
+    const targetMap = new Map(subgraphsByTarget.map((s) => [s.targetId, s]));
+    const baseMap = new Map(baseSubgraphsById.map((s) => [s.id, s]));
+
     const featureSubgraphs: FeatureSubgraphDTO[] = [];
     for (const f of featureSubgraphTargets) {
-      const fs = await subgraphRepo.byTargetId(f.targetId);
+      const fs = targetMap.get(f.targetId);
       if (!fs) {
         continue;
       }
 
-      const baseSubgraph = await subgraphRepo.byId(f.baseSubgraphId);
+      const baseSubgraph = baseMap.get(f.baseSubgraphId);
       if (!baseSubgraph) {
         continue;
       }

Note: This would require adding batch methods to SubgraphRepository if they don't exist.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controlplane/src/core/repositories/FeatureFlagRepository.ts` around lines 489
- 505, The loop over featureSubgraphTargets causes an N+1 query pattern by
calling subgraphRepo.byTargetId and subgraphRepo.byId per item; collect all
targetIds and baseSubgraphIds from featureSubgraphTargets first, add or use
batch methods on SubgraphRepository (e.g., byTargetIds(targetIds[]) and
byIds(baseIds[])) to fetch maps of Subgraph objects in two bulk queries, then
iterate featureSubgraphTargets and look up fs and baseSubgraph from those maps
when building featureSubgraphs (replace the per-item calls to byTargetId and
byId).
controlplane/src/core/repositories/SubgraphRepository.ts (1)

685-685: Add an explicit return type to the new static helper.

Callers branch on this value as a boolean, so : boolean should be part of the signature instead of being inferred.

Suggested change
-  static applyRbacConditionsToQuery(rbac: RBACEvaluator | undefined, conditions: (SQL<unknown> | undefined)[]) {
+  static applyRbacConditionsToQuery(
+    rbac: RBACEvaluator | undefined,
+    conditions: (SQL<unknown> | undefined)[],
+  ): boolean {

As per coding guidelines, **/*.{ts,tsx}: Use explicit type annotations for function parameters and return types in TypeScript.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controlplane/src/core/repositories/SubgraphRepository.ts` at line 685, The
static helper applyRbacConditionsToQuery currently relies on an inferred return
type but callers branch on it as a boolean—update its signature to explicitly
declare a boolean return type by adding ": boolean" to the method declaration
(static applyRbacConditionsToQuery(rbac: RBACEvaluator | undefined, conditions:
(SQL<unknown> | undefined)[]): boolean) and ensure the implementation returns a
boolean value in all code paths to satisfy the explicit annotation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.ts`:
- Around line 55-61: Remove the two raw console.log separator calls around the
subgraphRepo.listByFederatedGraph call in getFederatedGraphByName; either delete
them or replace with a single structured logger.debug (using the module's
logger) if you need trace info, and keep the call to
subgraphRepo.listByFederatedGraph({ federatedGraphTargetId:
federatedGraph.targetId, published: false, rbac: authContext.rbac }) unchanged.

In `@controlplane/src/core/repositories/SubgraphRepository.ts`:
- Around line 985-1023: The mapper in listByFederatedGraph is creating a
ProtoSubgraph for every gRPC subgraph even when schemaVersionId is missing and
it fails to populate plugin metadata for grpc_plugin; update the mapping so that
proto is only created when sg.schemaVersionId (or svSchemaSDL equivalent) exists
(matching getSubgraph/byTargetId behavior) and, when sg.type === 'grpc_plugin',
populate proto.pluginData with the plugin image/metadata fields (the same
properties getSubgraph used previously — e.g., plugin image/name/version fields
from the sg record) so the SubgraphDTO/ProtoSubgraph contract is preserved
between listByFederatedGraph and getSubgraph.
- Around line 975-981: The join on schema.protobufSchemaVersions currently
switches between innerJoin and leftJoin based on data.published which causes
non-gRPC subgraphs to be dropped when published is true; change the dynamic join
to always use leftJoin for the protobufSchemaVersions join (the call that
currently uses [data.published ? 'innerJoin' : 'leftJoin'] with
schema.protobufSchemaVersions and the ON clause containing
inArray(schema.subgraphs.type, ['grpc_plugin','grpc_service']) and
eq(schema.subgraphs.schemaVersionId,
schema.protobufSchemaVersions.schemaVersionId)), and make the same change in the
analogous joins in FederatedGraphRepository (the recomposition logic) and
createFederatedGraph so protobuf fields remain nullable and non-gRPC subgraphs
are preserved.

---

Outside diff comments:
In `@controlplane/src/core/repositories/FederatedGraphRepository.ts`:
- Around line 487-490: In FederatedGraphRepository (the mixed RBAC branch inside
the list/filter construction), change the left-hand field used for namespace
checks from schema.targets.id to schema.targets.namespaceId so namespace-scoped
permissions are matched correctly; specifically, update the or(...) call that
currently does or(inArray(schema.targets.id, [...new Set(namespaces)]),
inArray(schema.targets.id, [...new Set(resources)])) to use
schema.targets.namespaceId for the namespaces side while keeping
schema.targets.id for resource-scoped checks and the existing de-duplication
with new Set.

---

Duplicate comments:
In `@controlplane/src/core/repositories/FeatureFlagRepository.ts`:
- Around line 467-468: The ORDER BY in FeatureFlagRepository using
.orderBy(asc(targets.createdAt)) is not deterministic and can cause
duplicates/skips during offset paging; update the query that builds the ordering
(the call chaining ending with .orderBy(...)) to add a stable tiebreaker such as
the primary key (e.g., targets.id or targets.pk) and include it with the same
sort direction (e.g., .orderBy(asc(targets.createdAt), asc(targets.id))) so rows
with identical createdAt values are consistently ordered.

---

Nitpick comments:
In `@controlplane/src/core/repositories/FeatureFlagRepository.ts`:
- Around line 251-253: The condition in FeatureFlagRepository (variable rbac)
redundantly checks rbac.isOrganizationAdminOrDeveloper and
rbac.isOrganizationViewer; since RBACEvaluator's isOrganizationViewer already
includes isOrganizationAdminOrDeveloper, simplify the guard to only check for
absence of rbac or rbac.isOrganizationViewer (i.e., replace the current if
(!rbac || rbac.isOrganizationAdminOrDeveloper || rbac.isOrganizationViewer)
return true; with if (!rbac || rbac.isOrganizationViewer) return true;),
referencing the RBACEvaluator properties to justify the change.
- Around line 489-505: The loop over featureSubgraphTargets causes an N+1 query
pattern by calling subgraphRepo.byTargetId and subgraphRepo.byId per item;
collect all targetIds and baseSubgraphIds from featureSubgraphTargets first, add
or use batch methods on SubgraphRepository (e.g., byTargetIds(targetIds[]) and
byIds(baseIds[])) to fetch maps of Subgraph objects in two bulk queries, then
iterate featureSubgraphTargets and look up fs and baseSubgraph from those maps
when building featureSubgraphs (replace the per-item calls to byTargetId and
byId).

In `@controlplane/src/core/repositories/SubgraphRepository.ts`:
- Line 685: The static helper applyRbacConditionsToQuery currently relies on an
inferred return type but callers branch on it as a boolean—update its signature
to explicitly declare a boolean return type by adding ": boolean" to the method
declaration (static applyRbacConditionsToQuery(rbac: RBACEvaluator | undefined,
conditions: (SQL<unknown> | undefined)[]): boolean) and ensure the
implementation returns a boolean value in all code paths to satisfy the explicit
annotation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 45080fc1-6313-4c83-86f4-56a00831d643

📥 Commits

Reviewing files that changed from the base of the PR and between 41890f4 and 440588e.

📒 Files selected for processing (6)
  • controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.ts
  • controlplane/src/core/bufservices/workspace/getWorkspace.ts
  • controlplane/src/core/repositories/FeatureFlagRepository.ts
  • controlplane/src/core/repositories/FederatedGraphRepository.ts
  • controlplane/src/core/repositories/SubgraphRepository.ts
  • controlplane/src/core/services/WorkspaceService.ts

Comment on lines +975 to +981
[data.published ? 'innerJoin' : 'leftJoin'](
schema.protobufSchemaVersions,
and(
inArray(schema.subgraphs.type, ['grpc_plugin', 'grpc_service']),
eq(schema.subgraphs.schemaVersionId, schema.protobufSchemaVersions.schemaVersionId),
),
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "SubgraphRepository.ts" | head -20

Repository: wundergraph/cosmo

Length of output: 119


🏁 Script executed:

find . -type f -name "FederatedGraphRepository.ts" | head -20

Repository: wundergraph/cosmo

Length of output: 125


🏁 Script executed:

find . -type f -name "createFederatedGraph.ts" | head -20

Repository: wundergraph/cosmo

Length of output: 136


🏁 Script executed:

wc -l ./controlplane/src/core/repositories/SubgraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 124


🏁 Script executed:

sed -n '960,990p' ./controlplane/src/core/repositories/SubgraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 1465


🏁 Script executed:

sed -n '930,965p' ./controlplane/src/core/repositories/SubgraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 1579


🏁 Script executed:

sed -n '900,935p' ./controlplane/src/core/repositories/SubgraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 1088


🏁 Script executed:

grep -n "listByFederatedGraph" ./controlplane/src/core/repositories/FederatedGraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 204


🏁 Script executed:

grep -n "listByFederatedGraph" ./controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts

Repository: wundergraph/cosmo

Length of output: 128


🏁 Script executed:

sed -n '1440,1450p' ./controlplane/src/core/repositories/FederatedGraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 501


🏁 Script executed:

sed -n '1544,1554p' ./controlplane/src/core/repositories/FederatedGraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 603


🏁 Script executed:

sed -n '194,204p' ./controlplane/src/core/bufservices/federated-graph/createFederatedGraph.ts

Repository: wundergraph/cosmo

Length of output: 444


🏁 Script executed:

sed -n '982,1020p' ./controlplane/src/core/repositories/SubgraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 1520


🏁 Script executed:

sed -n '1020,1045p' ./controlplane/src/core/repositories/SubgraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 1075


Always use leftJoin for the protobufSchemaVersions join to prevent silently dropping non-gRPC subgraphs.

When published is true, this becomes an innerJoin whose ON clause filters for only grpc_plugin and grpc_service types. Non-gRPC subgraphs won't match this condition and are silently excluded from the result set. This occurs in both FederatedGraphRepository.ts (line 1547) and createFederatedGraph.ts (line 197) during graph recomposition, potentially causing regular GraphQL subgraphs to disappear from the composed graph without warning. The codebase already handles NULL proto fields correctly (lines 987-997), so using leftJoin consistently is safe and will preserve all subgraphs.

Suggested change
-      [data.published ? 'innerJoin' : 'leftJoin'](
+      .leftJoin(
         schema.protobufSchemaVersions,
         and(
           inArray(schema.subgraphs.type, ['grpc_plugin', 'grpc_service']),
           eq(schema.subgraphs.schemaVersionId, schema.protobufSchemaVersions.schemaVersionId),
         ),
       )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controlplane/src/core/repositories/SubgraphRepository.ts` around lines 975 -
981, The join on schema.protobufSchemaVersions currently switches between
innerJoin and leftJoin based on data.published which causes non-gRPC subgraphs
to be dropped when published is true; change the dynamic join to always use
leftJoin for the protobufSchemaVersions join (the call that currently uses
[data.published ? 'innerJoin' : 'leftJoin'] with schema.protobufSchemaVersions
and the ON clause containing inArray(schema.subgraphs.type,
['grpc_plugin','grpc_service']) and eq(schema.subgraphs.schemaVersionId,
schema.protobufSchemaVersions.schemaVersionId)), and make the same change in the
analogous joins in FederatedGraphRepository (the recomposition logic) and
createFederatedGraph so protobuf fields remain nullable and non-gRPC subgraphs
are preserved.

Comment on lines +985 to +1023
// Transform the selected subgraphs into SubgraphDTO objects
return subgraphs.map((sg) => {
let proto: ProtoSubgraph | undefined;
if (sg.type === 'grpc_plugin' || sg.type === 'grpc_service') {
if (!sg.protoSchemaVersion) {
this.logger.warn(
`Missing protobuf schema for ${sg.type} subgraph with schemaVersionId: ${sg.schemaVersionId}`,
);
}

for (const target of targets) {
const sg = await this.byTargetId(target.id);
if (sg === undefined) {
continue;
proto = {
schema: sg.protoSchemaVersion ?? '',
mappings: sg.protoMappings ?? '',
lock: sg.protoLock ?? '',
};
}
subgraphs.push(sg);
}

return subgraphs;
return {
id: sg.id,
targetId: sg.targetId,
routingUrl: sg.routingUrl,
readme: sg.readme || undefined,
subscriptionUrl: sg.subscriptionUrl || '',
subscriptionProtocol: sg.subscriptionProtocol ?? 'ws',
websocketSubprotocol: sg.websocketSubprotocol || undefined,
name: sg.name,
schemaSDL: sg.svSchemaSDL ?? '',
schemaVersionId: sg.schemaVersionId || '',
lastUpdatedAt: sg.svLastUpdated?.toISOString() ?? '',
labels: sg.labels?.map?.((l) => splitLabel(l)) ?? [],
creatorUserId: sg.createdBy || undefined,
namespace: sg.namespaceName,
namespaceId: sg.namespaceId,
isEventDrivenGraph: sg.isEventDrivenGraph,
isV2Graph: sg.svIsV2Graph || undefined,
isFeatureSubgraph: sg.isFeatureSubgraph,
type: sg.type,
proto,
};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "SubgraphRepository.ts" -type f

Repository: wundergraph/cosmo

Length of output: 119


🏁 Script executed:

wc -l ./controlplane/src/core/repositories/SubgraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 124


🏁 Script executed:

sed -n '980,1030p' ./controlplane/src/core/repositories/SubgraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 1827


🏁 Script executed:

grep -n "byTargetId" ./controlplane/src/core/repositories/SubgraphRepository.ts | head -20

Repository: wundergraph/cosmo

Length of output: 455


🏁 Script executed:

sed -n '1135,1200p' ./controlplane/src/core/repositories/SubgraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 1960


🏁 Script executed:

sed -n '1025,1050p' ./controlplane/src/core/repositories/SubgraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 1166


🏁 Script executed:

sed -n '1025,1130p' ./controlplane/src/core/repositories/SubgraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 4117


🏁 Script executed:

sed -n '930,985p' ./controlplane/src/core/repositories/SubgraphRepository.ts

Repository: wundergraph/cosmo

Length of output: 2548


🏁 Script executed:

rg -n "ProtoSubgraph" ./controlplane/src/core/repositories/SubgraphRepository.ts | head -10

Repository: wundergraph/cosmo

Length of output: 239


🏁 Script executed:

sed -n '1,100p' ./controlplane/src/core/repositories/SubgraphRepository.ts | head -80

Repository: wundergraph/cosmo

Length of output: 2274


🏁 Script executed:

rg -n "interface ProtoSubgraph\|type ProtoSubgraph" . --type ts

Repository: wundergraph/cosmo

Length of output: 43


🏁 Script executed:

find . -name "*.ts" -type f -exec grep -l "ProtoSubgraph" {} \; | head -10

Repository: wundergraph/cosmo

Length of output: 265


🏁 Script executed:

grep -n "ProtoSubgraph\|pluginData" ./controlplane/src/types/index.ts | head -30

Repository: wundergraph/cosmo

Length of output: 147


🏁 Script executed:

sed -n '111,120p' ./controlplane/src/types/index.ts

Repository: wundergraph/cosmo

Length of output: 218


Restore the old gRPC SubgraphDTO semantics in the mapper.

The new listByFederatedGraph mapper creates proto for every gRPC subgraph regardless of whether schemaVersionId exists, and it never populates proto.pluginData for grpc_plugin. The previous getSubgraph() implementation (used by byTargetId()) only created proto when schemaVersionId was not null and included plugin image metadata in pluginData for grpc_plugin types. This breaks the DTO contract and changes the behavior of callers relying on consistent semantics between the two methods.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controlplane/src/core/repositories/SubgraphRepository.ts` around lines 985 -
1023, The mapper in listByFederatedGraph is creating a ProtoSubgraph for every
gRPC subgraph even when schemaVersionId is missing and it fails to populate
plugin metadata for grpc_plugin; update the mapping so that proto is only
created when sg.schemaVersionId (or svSchemaSDL equivalent) exists (matching
getSubgraph/byTargetId behavior) and, when sg.type === 'grpc_plugin', populate
proto.pluginData with the plugin image/metadata fields (the same properties
getSubgraph used previously — e.g., plugin image/name/version fields from the sg
record) so the SubgraphDTO/ProtoSubgraph contract is preserved between
listByFederatedGraph and getSubgraph.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controlplane/test/test-util.ts`:
- Line 119: The fallback default for Keycloak API URL in variable apiUrl was
changed to use port 8090; revert this so that const apiUrl =
process.env.KC_API_URL || 'http://localhost:8080' (restore port 8080) in
controlplane/test/test-util.ts so tests use the same Keycloak port as the test
environment when KC_API_URL is not set; update the literal fallback value only
(leave process.env.KC_API_URL usage intact).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2e01267e-afb0-4c4b-8381-32d9f97b94bd

📥 Commits

Reviewing files that changed from the base of the PR and between 440588e and ab799f9.

📒 Files selected for processing (3)
  • controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.ts
  • controlplane/src/core/repositories/SubgraphRepository.ts
  • controlplane/test/test-util.ts
💤 Files with no reviewable changes (1)
  • controlplane/src/core/bufservices/federated-graph/getFederatedGraphByName.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • controlplane/src/core/repositories/SubgraphRepository.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants